﻿package away3d.animators ;


import flash.events.EventDispatcher ;
import flash.geom.Vector3D ;
import away3d.extrusions.utils.PathUtils ;
import away3d.extrusions.utils.Path ;
import away3d.extrusions.utils.PathSegment ;
import away3d.events.PathEvent ;
import away3d.core.base.Object3D ;



class PathAnimator extends EventDispatcher
{
    
    
    private var _path           : Path ; 
    private var _time           : Float ;
    private var _index          : Int ;// = 0;
    private var _rotations      : Vector<Vector3D<Float>> ;//TODO: check
    private var _lookAt         : Bool ;
    private var _alignToPath    : Bool ;
    private var _target         : Object3D ;
    private var _lookAtTarget   : Object3D ;
    private var _offset         : Vector3D<Float> ;//TODO: check
    private var _tmpOffset      : Vector3D<Float> ;//TODO: check
    private var _position       : Vector3D<Float> ; //= new Vector3D();//TODO: check
    private var _lastTime       : Float ;
    private var _from           : Float ;
    private var _to             : Float ;
    private var _bRange         : Bool ;
    private var _bSegment       : Bool ;
    private var _bCycle         : Bool ;
    private var _lastSegment    : Int ;//= 0;
    private var _rot            : Vector3D<Float> ;//TODO: check
    private var _worldAxis      : Vector3D<Float> ;//TODO: check
    private var _basePosition   : Vector3D<Float> ; // = new Vector3D(0,0,0);//TODO: check
    
    /**
     * Creates a new <PathAnimator>PathAnimator</code>
     * 
     * @param   [optional] path     The path to animate onto.
     * @param   [optional] target   An Object3D, the object to animate along the path. It can be Mesh, Camera, ObjectContainer3D...
     * @param   [optional] alignToPath  Defines if the object animated along the path is orientated to the path. Default is true.
     * @param   [optional] lookAtTarget An Object3D that the target will constantly look at during animation.
     * @param   [optional] offset	    A Vector3D to define the target offset to its location on the path.
     * @param   [optional] rotations    A Vector.<Vector3D> to define rotations per pathsegments. If PathExtrude is used to simulate the "road", use the very same rotations vector.
     */
    public function new ( ?path         : Path                      = null
                        , ?target       : Object3D                  = null
                        , ?offset       : Vector3D<Float>           = null
                        , ?alignToPath  : Bool                      = true
                        , ?lookAtTarget : Object3D                  = null
                        , rotations_    : Vector<Vector3D<Float>>   = null
                        )
    {
        
        _index                  = 0 ;
        _time  = _lastTime      = 0 ;
        _path                   = path ;
        if( _path ) _worldAxis  = _path.worldAxis ;
        _target                 = target ;
        _alignToPath            = alignToPath ;
        _lookAtTarget           = lookAtTarget ;
        
        if( offset ) setOffset( offset.x, offset.y, offset.z ) ;
        
        rotations = rotations_;
        
        if( _lookAtTarget && _alignToPath ) _alignToPath = false ;
        
    }
    
    
    /**
     * sets an optional offset to the position on the path, ideal for cameras or reusing the same <code>Path</code> object for parallel animations
     */
    public function setOffset( ?x: Float = 0, ?y: Float = 0, z: Float = 0 )
    {
        
        if( !_offset ) _offset = new Vector3D() ;
        
        _offset.x = x ;
        _offset.y = y ;
        _offset.z = z ;
        
    }
    
    
    /**
     * Calculates the new position and set the object on the path accordingly
     *
     * @param t 	A Number  from 0 to 1  (less than one to allow alignToPath)
     */
    public function updateProgress( t: Float )
    {
        
        //TODO: Check throw work ok like this
        if( !_path )  throw new Error("No Path object set for this class");
        
        if( t <= 0 )
        {
            
            t = 0 ;
            _lastSegment = 0 ;
            
        }
        else if( t >= 1 )
        {
            
            t = 1 ;
            _lastSegment = _path.length - 1 ;
            
        }
        
        if( _bCycle && t <= 0.1 && _lastSegment == _path.length - 1 ) dispatchEvent( new PathEvent( PathEvent.CYCLE ) ) ;
        
        _lastTime   = t ;
        var multi   = _path.length*t ;
        _index      = multi ;
        
        if( _index == _path.length ) index -- ;
        if( _offset != null ) _target.position = _basePosition ;
        
        var nT      = multi-_index;
        
        updatePosition( nT, PathSegment( _path.segments[ _index ] ) );
        
        var rotate:Bool;
        if( _lookAtTarget )
        {
            if( _offset )
            {
                
                _target.moveRight( _offset.x ) ;
                _target.moveUp( _offset.y ) ;
                _target.moveForward( _offset.z ) ;
                
            }
            
            _target.lookAt(_lookAtTarget.position) ;
            
        } 
        else if( _alignToPath )
        {
            
            if( _rotations && _rotations.length > 0 )
            {
                
                if( _rotations[ _index + 1 ] == null )
                {
                    
                    _rot.x = _rotations[ _rotations.length - 1 ].x*nT ;
                    _rot.y = _rotations[ _rotations.length - 1 ].y*nT ;
                    _rot.z = _rotations[ _rotations.length - 1 ].z*nT ; 
                    
                }
                else
                {
                    
                    _rot.x = _rotations[ _index ].x +   ( ( _rotations[ _index + 1 ].x -_rotations[ _index ].x )*nT ) ;
                    _rot.y = _rotations[ _index ].y +   ( ( _rotations[ _index + 1 ].y -_rotations[ _index ].y )*nT ) ;
                    _rot.z = _rotations[ _index ].z +   ( ( _rotations[ _index + 1 ].z -_rotations[ _index ].z )*nT ) ;
                    
                }
                
                _worldAxis.x    = 0;
                _worldAxis.y    = 1;
                _worldAxis.z    = 0;
                _worldAxis      = PathUtils.rotatePoint( _worldAxis, _rot ) ;
                
                _target.lookAt(_basePosition, _worldAxis) ;
                
                rotate = true;
                
            }
            else
            {
                _target.lookAt(_position) ;
            }
            
        }
        
        updateObjectPosition( rotate ) ;
        
        if( _bSegment && _index > 0 && _lastSegment != _index && t < 1 )
        {
            
            dispatchEvent( new PathEvent( PathEvent.CHANGE_SEGMENT ) ) ;
            
        }
        
        if( _bRange &&( t >= _from && t <= _to ) )
        {
            
            dispatchEvent( new PathEvent( PathEvent.RANGE ) ) ;
            
        }
        
        _time           = t ;
        _lastSegment    = _index ;
    
    }
    
    
    /**
     * Updates a position Vector3D on the path at a given time. Do not use this handler to animate, 
     * it's in there to add dummy's or place camera before or after
     * the animated object. Use the update() or the automatic tweened animateOnPath() handlers instead.
     *
     * @param t      Number. A Number  from 0 to 1
     * @param out    Vector3D. The Vector3D to update according to the "t" time parameter.
     */
    public function getPositionOnPath( t: Float, out: Vector3D<Float> ): Vector3D<Float>
    {
        
        if( !_path )  throw new Error( "No Path object set for this class" ) ;
        
        t = ( t < 0 )? 0 : ( t > 1 )?1 : t ;
        
        var m   = _path.length*t ;
        var i   = m ;
        var ps  = _path.segments[ i ] ;
        
        return calcPosition( m - i, ps, out ) ;
        
    }
    
    
    /**
     * Returns a position on the path according to duration/elapsed time. Duration variable must be set.
     * 
     * @param   ms          Number. A number representing milliseconds.
     * @param   duration    Number. The total duration in milliseconds.
     * @param   out         [optional] Vector3D. A Vector3D that will be used to return the position. 
     *                      If none provided, method returns a new Vector3D with this data.
     *
     * An example of use of this handler would be cases where a given "lap" must be done in a given amount 
     * of time and you would want to retrieve the "ideal" time based on elapsed time since start of the race. 
     * By comparing actual progress to ideal time, you could extract their classement, 
     * calculate distance/time between competitors,
     * abort the race if goal is impossible to be reached in time etc...
     *
     *@ returns Vector3D The position at a given elapsed time, compared to total duration.
     */
    public function getPositionOnPathMS( ms: Float, duration: Float, out: Vector3D<Float> ) : Vector3D<Float>
    {
        
        if( !_path ) throw new Error( "No Path object set for this class" ) ;
        
        var t : Float = Math.abs( ms ) / duration ;
        t       = ( t < 0 )? 0 : ( t > 1 )?1 : t ;
        var m   = _path.length*t ;
        var i   = m ;
        var ps  = _path.segments[ i ] ;
        
        return calcPosition( m - i, ps, out ) ;
        
    }
    
    
    public var alignToPath( get_alignToPath, set_alignToPath ) ;
    
    
    /**
     * defines if the object animated along the path must be aligned to the path.
     */
    public function set_alignToPath( b: Bool ) : Bool
    {
        
        _alignToPath = b ;
        return b ;
        
    }
    
    
    public function get_alignToPath() : Bool
    {
        
        return _alignToPath ;
        
    }
    
    public var position( get_position, null ): Vector3D<Float> ;
    
    /**
     * returns the current interpolated position on the path with no optional offset applied
     */
    public function get_position():Vector3D
    {
        
        return _position ;
        
    }
    
    
    public var path( get_path, set_path ) : Path ;
    
    
    /**
     * defines the path to follow
     * @see Path
     */
    public function set_path( p: Path ) : Path
    {
        
        _path       = p ;
        _worldAxis  = _path.worldAxis ;
        return p ;
        
    }
    
    
    public function get_path() : Path
    {
        
        return _path ;
        
    }
    
    
    public var progress( get_progress, set_progress ): Float ;
    
    
    /**
     * Represents the progress of the animation playhead from the start (0) to the end (1) of the animation.
     */
    public function get_progress() : Float
    {
        
        return _time ;
        
    }
    
    
    public function set progress( val: Float ) : Float
    {
        
        if( _time == val ) return ;
        
        updateProgress( val ) ;
        
    }
    
    
    /**
     * returns the segment index that is used at a given time;
     * @param	 t		[Number]. A Number between 0 and 1. If no params, actual pathanimator time segment index is returned.
     */
     //TODO: NaN ???
    public function getTimeSegment( t: Float = NaN ): Float
    {
        
        t = ( isNaN( t ) )? _time : t ;
        return Math.floor( path.length*t ) ;
        
    }
    
    
    public var orientation( get_orientation, set_orientation ) ;
    
    
    /**
     * returns the actual interpolated rotation along the path.
     */
    public function get_orientation(): Vector3D<Float>
    {
        
        return _rot ;
        
    }
    
    
    public var target( get_target, set_target ): Object3D ;
    
    
    /**
     * sets the object to be animated along the path.
     */
    public function set_target( object3d: Object3D ) : Object3D
    {
        
        _target = target ;
        _target ;
        
    }
    
    
    
    public function get_target() : Object3D
    {
        
        return _target ;
        
    }
    
    
    public var lookAtObject( get_lookAtObject, set_lookAtObject ) : Object3D ;
    
    
    /**
     * sets the object that the animated object will be looking at along the path
     */
    public function set_lookAtObject( object3d: Object3D ) : Object3D
    {
        
        _lookAtTarget = object3d ;
        if( _alignToPath ) _alignToPath = false ;
        return object3d ;
        
    }
    
    
    public function get_lookAtObject() : Object3D
    {
        
        return _lookAtTarget;
        
    }
    
    
    public var rotations( null, set_rotations ) : Vector<Vector3D> ;
    
    
    /**
     * sets an optional Vector.<Vector3D> of rotations. 
     *  if the object3d is animated along a PathExtrude object, use the very same vector to follow the "curves".
     */
    public function set_rotations( vRot: Vector<Vector3D> ): Vector<Vector3D>
    {
        
        _rotations = vRot ;
        
        if( _rotations && !_rot )
        {
            
            _rot        = new Vector3D() ;
            _tmpOffset  = new Vector3D() ;
            
        }
        
        return vRot ;
        
    }
    
    
    public var index( get_index, set_index ) : Int ;
    
    
    /**
     * Set the pointer to a given segment along the path
     */
    public function set_index( val: Int ) : Int
    {
        
        _index = ( val > _path.length - 1 )? _path.length - 1 : ( val > 0 )? val : 0 ;
        return val ;
        
    }
    
    
    public function get_index() : Int
    {
        
        return _index ;
        
    }
    
    
    /**
     * Default method for adding a cycle event listener. Event fired when the time reaches 1.
     * 
     * @param	listener		The listener function
     */
    public function addOnCycle( listener: Void -> Event )
    {
        
        _lastTime   = 0 ;
        _bCycle     = true ;
        this.addEventListener( PathEvent.CYCLE, listener ) ;
        
    }
    
    
    /**
     * Default method for removing a cycle event listener
     * 
     * @param		listener		The listener function
     */
    public function removeOnCycle( listener: Void -> Event )
    {
        
        _bCycle = false ;
        removeEventListener( PathEvent.CYCLE, listener ) ;
        
    }
    
    
    /**
     * Default method for adding a range event listener. Event fired when the time is >= from and <= to variables.
     * 
     * @param		listener		The listener function
     */
    //note: If there are requests for this, it could be extended to more than one rangeEvent per path. 
    public function addOnRange( listener: Void -> Event, ?from: Float = 0, ?to: Float = 0 )
    {
        
        _from   = from ;
        _to     = to ;
        _bRange = true ;
        addEventListener( PathEvent.RANGE, listener ) ;
        
    }
    
    
    /**
     * Default method for removing a range event listener
     * 
     * @param		listener		The listener function
     */
    public function removeOnRange( listener: Void -> Event )
    {
        
        _from   = 0 ;
        _to     = 0 ;
        _bRange = false ;
        removeEventListener( PathEvent.RANGE, listener );
        
    }
    
    
    /**
     * Default method for adding a segmentchange event listener. Event fired when the time pointer enters another PathSegment.
     * 
     * @param		listener		The listener function
     */
    public function addOnChangeSegment( listener: Void -> Event )
    {
        
        _bSegment       = true ;
        _lastSegment    = 0 ;
        addEventListener( PathEvent.CHANGE_SEGMENT, listener ) ;
        
    }
    
    
    /**
     * Default method for removing a range event listener
     * 
     * @param		listener		The listener function
     */
    public function removeOnChangeSegment( listener: Void -> Event )
    {
        
        _bSegment       = false ;
        _lastSegment    = 0 ;
        removeEventListener( PathEvent.CHANGE_SEGMENT, listener, false ) ;
        
    }
    
    
    private function calcPosition( t: Float, ps: PathSegment, out: Vector3D<Float> ) : Vector3D<Float>
    {
        
        var dt  = 2 * ( 1 - t ) ;
        var v   = out || new Vector3D() ;
        v.x     = ps.pStart.x + t * ( dt * ( ps.pControl.x - ps.pStart.x ) + t * ( ps.pEnd.x - ps.pStart.x ) ) ;
        v.y     = ps.pStart.y + t * ( dt * ( ps.pControl.y - ps.pStart.y ) + t * ( ps.pEnd.y - ps.pStart.y ) ) ;
        v.z     = ps.pStart.z + t * ( dt * ( ps.pControl.z - ps.pStart.z ) + t * ( ps.pEnd.z - ps.pStart.z ) ) ;
        
        return v ;
        
    }
    
    
    private function updatePosition( t: Float, ps: PathSegment )
    {
        
        _basePosition   = calcPosition( t, ps, _basePosition) ;
        _position.x     = _basePosition.x ;
        _position.y     = _basePosition.y ;
        _position.z     = _basePosition.z ;
        
    }
    
    
    private function updateObjectPosition( ?rotate: Bool = false )
    {
        
        if( rotate && _offset )
        {
            
            _tmpOffset.x    = _offset.x ;
            _tmpOffset.y    = _offset.y ;
            _tmpOffset.z    = _offset.z ;
            _tmpOffset      = PathUtils.rotatePoint(  _tmpOffset, _rot ) ;
            
            _position.x     += _tmpOffset.x ;
            _position.y     += _tmpOffset.y ;
            _position.z     += _tmpOffset.z ;
            
        } 
        else if( _offset )
        {
            
            _position.x     += _offset.x ;
            _position.y     += _offset.y ;
            _position.z     += _offset.z ;
            
        } 
        
        _target.position = _position ;
        
    }
    
    
}